19. ContentProvider update() Method

Now it's time for you to write the other provider methods. Let's do update() next, since it's similar to insert().

Overview of the update() Method

Looking at the https://developer.android.com/reference/android/content/ContentProvider.html#update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[])" target="_blank">documentation for the ContentProvider update() method, notice the inputs and output to the method. The input parameters are a Uri, ContentValues object, as well as selection and selectionArgs. The return value is the number of rows that were successfully updated.

Here’s an end-to-end diagram of how the update() method works.

Based on the result from the UriMatcher, the PETS and PET_ID cases are both supported. In the case of PETS, the caller wants to update multiple rows in the pets table according to the selection and selection args. In the case of PET_ID, the caller wants to update a specific pet. The new values that will be written to the database are contained in the ContentValues object passed into the method.

We can see how the return value is an integer, for the number of rows affected.

Sample Use Case #1

Say for example, we want to update our dummy pet entries for Toto to be a different pet - Milo, the french bulldog! Look for @frenchiebutt on instagram for some seriously adorable photos. Since both of them are male, there’s no need to update gender. We’ll just include 3 attributes in the ContentValues: name, breed, and weight.

Example inputs to update() method:

URI: content://com.example.android.pets/pets/
ContentValues: name is Milo, breed is French bulldog, weight is 20
Selection: “name=?”
SelectionArgs: { “Toto” }

Within the update() method:

SQLite statement: UPDATE pets SET name = ‘Milo’, breed=’French bulldog’, weight=20 WHERE name=‘Toto’

Result:

If we started off with 3 Toto’s in our pet table, then a successful update operation would return the number 3 - for 3 rows being updated to have Milo’s attributes.

Sample Use Case #2

Say for example, we want to update a single pet (such as Tommy) to be Milo. This time, instead of passing in a URI with selection and selection args, we’ll just pass in the specific content URI for Tommy (such as content://com.example.android.pets/pets/5). Since both of them are male, there’s no need to update gender. We’ll just include 3 attributes in the ContentValues: name, breed, and weight.

Example inputs to update() method:

URI: content://com.example.android.pets/pets/5
ContentValues: name is Milo, breed is French bulldog, weight is 20

Within the update() method:

SQLite statement: UPDATE pets SET name = ‘Milo’, breed=’French bulldog’, weight=20 WHERE _id=5

Result:

A successful update operation would return 1, for one row being updated to have Milo’s attributes (specifically row #5).

Code for the update() Method

Let’s move onto implementing the code now. Here's a gist to get started. Replace the current update() method in your PetProvider class with the one provided below. Also, add the updatePet() helper method we’ve provided.

In PetProvider.java:

public class PetProvider extends ContentProvider {

    …

    @Override
    public int update(Uri uri, ContentValues contentValues, String selection,
                      String[] selectionArgs) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case PETS:
                return updatePet(uri, contentValues, selection, selectionArgs);
            case PET_ID:
                // For the PET_ID code, extract out the ID from the URI,
                // so we know which row to update. Selection will be "_id=?" and selection
                // arguments will be a String array containing the actual ID.
                selection = PetEntry._ID + "=?";
                selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
                return updatePet(uri, contentValues, selection, selectionArgs);
            default:
                throw new IllegalArgumentException("Update is not supported for " + uri);
        }
    }

    /**
     * Update pets in the database with the given content values. Apply the changes to the rows
     * specified in the selection and selection arguments (which could be 0 or 1 or more pets).
     * Return the number of rows that were successfully updated.
     */
    private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

        // TODO: Update the selected pets in the pets database table with the given ContentValues

        // TODO: Return the number of rows that were affected
        return 0;
    }

    …
}

You’ll notice that the PETS and PET_ID cases both call the updatePet() method to do the actual database operation. The only difference is that in the PET_ID case, we have 2 more lines of code to manually set the selection string and selection arguments array to pinpoint a single pet based on the incoming pet URI. Similar to the logic in the PetProvider query() method, we set the selection string to be “id=?” and the selection args to be the row ID that we care about (by extracting out the ID from the URI using ContentUris.parseId() method). Here are the main steps happening in the update() method.

The provider update() method returns the number of rows that were affected. If you try to call the update() method with an empty ContentValues object, the provider should return that 0 rows were updated.

Your Task: Finish the TODO

There is a TODO in the code left for you to complete. Based on comment on the updatePet() helper method, you’ll see that this method is supposed to do the actual database update operation.

Sanity check the data in the ContentValues object

Remember to sanity check your data. Since you’re inserting new data into the database, make sure the name, breed, gender, and weight values meet the requirements that we outlined in the previous quiz.

While the requirements on the data are the same as in the insert() method, there is one key difference. For the insert() method, it’s a brand new pet, so ALL attributes (except for breed) should be present. With the update() method, you DON’T NEED ALL four attributes to be in the ContentValues object. You might only update one attribute, such as the breed. In this case, the fields that you're not updating don’t need to be present in the ContentValues object. The values for the fields (that aren’t present in the ContentValues object) will stay the same as before.

Because not all attributes will be present, we encourage you to use the ContentValues containsKey() method to check whether a key/value pair exists, before trying to check if it has a reasonable value.

When you’re done, ensure that your app still compiles and runs on your device. You won’t need to update the UI code to call the provider update() method yet. That will come in Lesson 4.

Okay, enough talk, more coding. Good luck!

Update Method

Implement PetProvider update() method

SOLUTION:
  • Replace the `update()` method in the `PetProvider` class using this gist
  • Add the `updatePet()` method we provided
  • Finish the TODO in the `updatePet()` helper method. Remember to sanity check the pet attributes in the `ContentValues`, if they are present

Hint: ContentValues containsKey() method checks if a key exists in the ContentValues

ContentProvider Update() Method - Solution

In the PetProvider update() method, perform sanity checks for each of the possible updated values. First ,we use ContentValues containsKey() method to check if each attribute is present or not. If the key is present, then we can proceed with extracting the value from it, and then checking if it’s valid.

Another way to think about this code change is that we’re wrapping an “if” check around the code block for each pet attribute (from the insertPet() method) and making sure the attribute is present first.

In PetProvider.java:

private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    // If the {@link PetEntry#COLUMN_PET_NAME} key is present,
    // check that the name value is not null.
    if (values.containsKey(PetEntry.COLUMN_PET_NAME)) {
        String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
        if (name == null) {
            throw new IllegalArgumentException("Pet requires a name");
        }
    }

    // If the {@link PetEntry#COLUMN_PET_GENDER} key is present,
    // check that the gender value is valid.
    if (values.containsKey(PetEntry.COLUMN_PET_GENDER)) {
        Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
        if (gender == null || !PetEntry.isValidGender(gender)) {
            throw new IllegalArgumentException("Pet requires valid gender");
        }
    }

    // If the {@link PetEntry#COLUMN_PET_WEIGHT} key is present,
    // check that the weight value is valid.
    if (values.containsKey(PetEntry.COLUMN_PET_WEIGHT)) {
        // Check that the weight is greater than or equal to 0 kg
        Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
        if (weight != null && weight < 0) {
            throw new IllegalArgumentException("Pet requires valid weight");
        }
    }

    // No need to check the breed, any value is valid (including null).

    …

This is also a chance to do a quick check on the size of the ContentValues object. If there are no key/value pairs in it, then just return 0 rows affected. There is no need to do the database operation if there are no new values to update with, and every database operation costs some amount of memory resources on the device.

// If there are no values to update, then don't try to update the database
if (values.size() == 0) {
    return 0;
}

If we have some changes we actually want to make on the database, get a writeable database from the PetDbHelper (because we are performing edits on the data source). Once we have a SQLiteDatabase object, we call the update() on it and pass in the table name, new ContentValues, selection, and selectionArgs. The return value from the SQLiteDatabase update() method is the number of rows affected, so we can just return that directly.

In PetProvider.java:

@Override
public int update(Uri uri, ContentValues contentValues, String selection,
                  String[] selectionArgs) {
    final int match = sUriMatcher.match(uri);
    switch (match) {
        case PETS:
            return updatePet(uri, contentValues, selection, selectionArgs);
        case PET_ID:
            // For the PET_ID code, extract out the ID from the URI,
            // so we know which row to update. Selection will be "_id=?" and selection
            // arguments will be a String array containing the actual ID.
            selection = PetEntry._ID + "=?";
            selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
            return updatePet(uri, contentValues, selection, selectionArgs);
        default:
            throw new IllegalArgumentException("Update is not supported for " + uri);
    }
}

/**
 * Update pets in the database with the given content values. Apply the changes to the rows
 * specified in the selection and selection arguments (which could be 0 or 1 or more pets).
 * Return the number of rows that were successfully updated.
 */
private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    // If the {@link PetEntry#COLUMN_PET_NAME} key is present,
    // check that the name value is not null.
    if (values.containsKey(PetEntry.COLUMN_PET_NAME)) {
        String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
        if (name == null) {
            throw new IllegalArgumentException("Pet requires a name");
        }
    }

    // If the {@link PetEntry#COLUMN_PET_GENDER} key is present,
    // check that the gender value is valid.
    if (values.containsKey(PetEntry.COLUMN_PET_GENDER)) {
        Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
        if (gender == null || !PetEntry.isValidGender(gender)) {
            throw new IllegalArgumentException("Pet requires valid gender");
        }
    }

    // If the {@link PetEntry#COLUMN_PET_WEIGHT} key is present,
    // check that the weight value is valid.
    if (values.containsKey(PetEntry.COLUMN_PET_WEIGHT)) {
        // Check that the weight is greater than or equal to 0 kg
        Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
        if (weight != null && weight < 0) {
            throw new IllegalArgumentException("Pet requires valid weight");
        }
    }

    // No need to check the breed, any value is valid (including null).

    // If there are no values to update, then don't try to update the database
    if (values.size() == 0) {
        return 0;
    }

    // Otherwise, get writeable database to update the data
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    // Returns the number of database rows affected by the update statement
    return database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs);
}

We run the app to make sure it still compiles. The real test for whether this code is implemented correctly will be in Lesson 4 when we hook up the update functionality in the UI.

See the full code diff for this code checkpoint here.